﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32;
using WindowsAPI.Service;

namespace WindowsAPI.Hardware
{
    /// <summary>Класс-обёртка для реализации доступа к устройствам системы</summary>
    [Serializable]
    public class Device : MarshalByRefObject, ITreeItem<Device>, IEquatable<Device>
    {
        public struct DeviceStatus
        {
            public DM_Flag Flag;
            public uint ProbNum;

            public bool CheckFlag(DM_Flag flag) { return (Flag & flag) == flag; }

            public DM_Flag[] Flags
            {
                get
                {
                    var flags = (DM_Flag[])Enum.GetValues(typeof(DM_Flag));
                    var result = new List<DM_Flag>(flags.Length);
                    for(var i = 0; i < flags.Length; i++)
                        if(CheckFlag(flags[i]))
                            result.Add(flags[i]);
                    return result.ToArray();
                    //return Enum.GetValues(typeof(DM_Flag)).Cast<DM_Flag>().Where(CheckFlag).ToArray();
                }
            }

            public override string ToString()
            {
                var flags = Flags;
                //return flags.Aggregate(new StringBuilder(), (S, f) => S.AppendFormat("|{0}", f)).ToString().Remove(0, 1);
                var result = new StringBuilder();
                for(var i = 0; i < flags.Length; i++)
                    result.AppendFormat("|{0}", flags[i]);
                return result.ToString().Remove(0, 1);
            }
        }

        /* ------------------------------------------------------------------------------------------ */

        private static Device sf_RootDevice;

        /* ------------------------------------------------------------------------------------------ */

        public static Device Root
        {
            get
            {
                if(sf_RootDevice != null) return sf_RootDevice;
                IntPtr DevInst;
                if(SetupAPI.CM_Locate_DevNode(out DevInst, null) != CR_ReturnCode.Success)
                    Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
                return sf_RootDevice = new Device(DevInst);
            }
        }

        /* ------------------------------------------------------------------------------------------ */

        //http://tagievara.narod.ru/pages/articles/sec_extract_usb_devices.html
        public static List<KeyValuePair<string, IntPtr>> GetDevicesList()
        {
            IntPtr DevInst;
            var result = SetupAPI.CM_Locate_DevNode(out DevInst, null);
            Console.WriteLine(result);

            return GetDevicesList(DevInst);
        }

        public static List<KeyValuePair<string, IntPtr>> GetDevicesList(IntPtr DevInst)
        {
            if(DevInst == IntPtr.Zero) return new List<KeyValuePair<string, IntPtr>>();

            IntPtr NewDevInst;
            //int len;

            //Получение статуса устройств
            DM_Flag Status;
            uint ProbNum;
            var result = SetupAPI.CM_Get_DevNode_Status(out Status, out ProbNum, DevInst);
            Console.WriteLine(result);

            //Получение идентификатора устройства
            var device_id_buffer = GetId(DevInst);
            Console.WriteLine(device_id_buffer);

            //Получение описания устройства
            RegistryValueKind Kind;
            var driver_name = (string)GetProperty(DevInst, CM_DRP.FriendlyName, out Kind);
            Console.WriteLine(driver_name);

            //выводим информацию
            var value = new KeyValuePair<string, IntPtr>(driver_name, DevInst);
            var Result = new List<KeyValuePair<string, IntPtr>> { value };

            //ищем потомков
            result = SetupAPI.CM_Get_Child(out NewDevInst, DevInst);
            Console.WriteLine(result);
            Result.AddRange(GetDevicesList(NewDevInst));

            //ищем соседей
            result = SetupAPI.CM_Get_Sibling(out NewDevInst, DevInst);
            Console.WriteLine(result);
            if(result == CR_ReturnCode.Success)
                Result.AddRange(GetDevicesList(NewDevInst));

            return Result;
        }

        public static object GetProperty(IntPtr DevInst, CM_DRP Property, out RegistryValueKind Kind)
        {
            uint length = 0;
            var result = SetupAPI.CM_Get_DevNode_Registry_Property(DevInst, Property, out Kind, IntPtr.Zero,
                                                                   ref length);
            //Console.WriteLine(result);
            if(result == CR_ReturnCode.No_Such_Value)
                return null;

            object Result = null;
            using(var UsingBuffer = new UsingObject<IntPtr>(Marshal.AllocHGlobal((int)length), Marshal.FreeHGlobal))
            {
                var pBuffer = UsingBuffer.Object;
                result = SetupAPI.CM_Get_DevNode_Registry_Property(DevInst, Property, out Kind, pBuffer, ref length);
                if(result == CR_ReturnCode.Success)
                    switch(Kind)
                    {
                        case RegistryValueKind.String:
                        case RegistryValueKind.ExpandString:
                            Result = Marshal.PtrToStringAnsi(pBuffer);
                            break;

                        case RegistryValueKind.Binary:
                            Result = new byte[length];
                            for(var i = 0; i < length; i++)
                                ((byte[])Result)[i] = Marshal.ReadByte(pBuffer);
                            break;

                        case RegistryValueKind.DWord:
                            Result = Marshal.ReadInt32(pBuffer);
                            break;

                        case RegistryValueKind.QWord:
                            Result = Marshal.ReadInt64(pBuffer);
                            break;

                        case RegistryValueKind.MultiString:
                        case RegistryValueKind.Unknown:
                            throw new NotImplementedException();
                        //default:
                        //    break;
                    }
            }
            return Result;
        }

        public static string GetId(IntPtr DevInst)
        {
            int DevLen;
            var result = SetupAPI.CM_Get_Device_ID_Size(out DevLen, DevInst);
            Console.WriteLine(result);
            DevLen++;

            var device_id_buffer = new StringBuilder(DevLen);
            result = SetupAPI.CM_Get_Device_ID(DevInst, device_id_buffer, device_id_buffer.Capacity);
            Console.WriteLine(result);
            return device_id_buffer.ToString();
        }

        public static IEnumerable<Device> USBSTOR
        {
            get
            {
                return Root.OrderWalk().Select(d => d.Item).Where(d => d.EnumeratorName == "USBSTOR");
                //foreach(var item in ITreeItemEx.OrderWalk(Root))
                //{
                //    var device = item.Item;
                //    if(device.EnumeratorName == "USBSTOR")
                //        yield return device;
                //}
            }
        }

        public static void RemoveUSBSTOR(bool Silent = true)
        {
            USBSTOR.Select(d => d.Parent).ForeachParralel(EjectDevice);
            //foreach(var device in USBSTOR) EjectDevice(device.Parent);
        }

        public static void RemoveUSBSTOR(Func<Device, bool> Selector, bool Silent = true)
        {
            USBSTOR.Where(Selector).Select(d => d.Parent).ForeachParralel(EjectDevice);
            //foreach(var device in USBSTOR) if(Selector(device)) EjectDevice(device.Parent);
        }

        private static void EjectDevice(Device device)
        {
            string ErrorMessage;
            if(device.Eject(out ErrorMessage) != CR_ReturnCode.Success)
                Console.WriteLine("Извлечение устройства {0} невозможно по причине:\r\n{1}", device, ErrorMessage);
        }

        public static IEnumerable<Guid> Classes
        {
            get
            {
                var i = 0u;
                Guid ClassGuid;
                var result = CfgMgr32.CM_Enumerate_Classes(i++, out ClassGuid);
                while(result == CR_ReturnCode.Success)
                {
                    yield return ClassGuid;
                    result = CfgMgr32.CM_Enumerate_Classes(i++, out ClassGuid);
                }
            }
        }

        public static string GetClassInfo(Guid ClassGuid)
        {
            var Buffer = new StringBuilder(1024);
            var rSize = 0;
            var result =
                SetupAPI.SetupDiGetClassDescription(ref ClassGuid, Buffer, Buffer.Capacity, ref rSize);
            return result ? Buffer.ToString() : "";
        }

        public static IEnumerable<SP_DEVINFO_DATA> GetDevices(Guid ClassGuid)
        {
            var hDeviceInfoSet = SetupAPI.SetupDiGetClassDevs(ref ClassGuid, null, IntPtr.Zero, DIGCF.Present);

            {
                var ihDeviceInfoSet = hDeviceInfoSet.ToInt32();
                if(ihDeviceInfoSet == -1 || ihDeviceInfoSet == 0)
                    yield break;
            }

            var i = 0;
            var DeviceInterfaceData = SP_DEVINFO_DATA.Default;
            var result = SetupAPI.SetupDiEnumDeviceInfo(hDeviceInfoSet, i++, ref DeviceInterfaceData);
            while(result)
            {
                yield return DeviceInterfaceData;
                DeviceInterfaceData = SP_DEVINFO_DATA.Default;
                result = SetupAPI.SetupDiEnumDeviceInfo(hDeviceInfoSet, i++, ref DeviceInterfaceData);
            }

            SetupAPI.SetupDiDestroyDeviceInfoList(hDeviceInfoSet);
        }

        /* ------------------------------------------------------------------------------------------ */

        private readonly IntPtr f_Handle;

        private string f_FrendlyName;
        private string f_EnumeratorName;

        private readonly Device f_PrewDevice;

        /* ------------------------------------------------------------------------------------------ */

        public IntPtr Handle { get { return f_Handle; } }

        public string FrendlyName
        {
            get
            {
                if(f_FrendlyName != null) return f_FrendlyName;
                return f_FrendlyName = GetProperty(CM_DRP.FriendlyName) as string ?? "";
            }
        }

        public string EnumeratorName
        {
            get
            {
                if(f_EnumeratorName != null) return f_EnumeratorName;
                return f_EnumeratorName = GetProperty(CM_DRP.EnumeratorName) as string ?? "";
            }
        }

        public Device Parent
        {
            get
            {
                IntPtr NewDevInst;
                var result = SetupAPI.CM_Get_Parent(out NewDevInst, f_Handle);
                if(result != CR_ReturnCode.Success)
                    Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
                return NewDevInst == IntPtr.Zero ? null : new Device(NewDevInst);
            }
            set { throw new NotSupportedException(); }
        }

        public Device Child
        {
            get
            {
                IntPtr NewDevInst;
                var result = SetupAPI.CM_Get_Child(out NewDevInst, f_Handle);
                if(result != CR_ReturnCode.Success)
                    Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
                return NewDevInst == IntPtr.Zero ? null : new Device(NewDevInst);
            }
            set { throw new NotSupportedException(); }
        }

        public Device Prev
        {
            get { return f_PrewDevice; }
            set { throw new NotSupportedException(); }
        }

        public Device Next
        {
            get
            {
                IntPtr NewDevInst;
                var result = SetupAPI.CM_Get_Sibling(out NewDevInst, f_Handle);
                if(result != CR_ReturnCode.Success)
                    Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
                return NewDevInst == IntPtr.Zero ? null : new Device(NewDevInst, this);
            }
            set { throw new NotSupportedException(); }
        }

        public DeviceStatus Status
        {
            get
            {
                DM_Flag status;
                uint ProbNum;
                var result = SetupAPI.CM_Get_DevNode_Status(out status, out ProbNum, f_Handle);
                if(result != CR_ReturnCode.Success && result != CR_ReturnCode.No_Such_DevNode)
                    Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
                return new DeviceStatus { Flag = status, ProbNum = ProbNum };
            }
        }

        public string Name
        {
            get
            {
                var handle = f_Handle;
                var buffer = new StringBuilder(Consts.MAX_PATH);

                var propRegDataType = 0;
                var reqSize = 0;

                var DevData = SP_DEVINFO_DATA.Default;
                SetupAPI.SetupDiGetDeviceRegistryProperty(handle, ref DevData, SPDRP.FriendlyName,
                    ref propRegDataType, buffer, buffer.Capacity, ref reqSize);
                if(buffer.Length == 0) return "";

                reqSize = 0;
                propRegDataType = 0;

                SetupAPI.SetupDiGetDeviceRegistryProperty(handle, ref DevData, SPDRP.DeviceDesc, ref propRegDataType,
                    buffer, Consts.MAX_PATH, ref reqSize);

                return buffer.ToString();
            }
        }

        /* ------------------------------------------------------------------------------------------ */

        public Device(IntPtr DevInst, Device Prew = null)
        {
            f_Handle = DevInst;
            f_PrewDevice = Prew;
        }

        /* ------------------------------------------------------------------------------------------ */

        public object GetProperty(CM_DRP Property)
        {
            RegistryValueKind Kind;
            return GetProperty(f_Handle, Property, out Kind);
        }

        public object GetProperty(CM_DRP Property, out RegistryValueKind Kind)
        {
            return GetProperty(f_Handle, Property, out Kind);
        }

        public CR_ReturnCode Eject(bool Silent = false)
        {
            var Veto = default(PNP_VETO_TYPE);
            var buffer = Silent ? new StringBuilder(Consts.MAX_PATH) : null;
            var result = SetupAPI.CM_Request_Device_Eject(f_Handle, ref Veto, buffer, Silent ? buffer.Capacity : 0);
            Console.WriteLine(result);
            if(result != CR_ReturnCode.Success && Silent)
                Console.WriteLine(buffer);
            return result;
        }

        public CR_ReturnCode Eject(out string Message)
        {
            var Veto = default(PNP_VETO_TYPE);
            var buffer = new StringBuilder(Consts.MAX_PATH);
            var result = SetupAPI.CM_Request_Device_Eject(f_Handle, ref Veto, buffer, buffer.Capacity);
            Console.WriteLine(result);
            Message = buffer.ToString();
            if(result != CR_ReturnCode.Success)
                Console.WriteLine(Message);
            return result;
        }


        //[NotImplemented, Link("http://articles.org.ru/forum/index.php?showtopic=1237")]
        //public bool ChangeState(bool Enable)
        //        {
        //            var DICS_State = Enable ? DICS.Enable : DICS.Disable;

        //            //throw new NotImplementedException("http://articles.org.ru/forum/index.php?showtopic=1237");

        //#pragma warning disable 162
        //            // ReSharper disable HeuristicUnreachableCode
        //            var Result = false;

        //            var DevInfoData = SP_DEVINFO_DATA.Default;
        //            IntPtr hDevInfo1;

        //            var DataT = 0;

        //            StringBuilder Buffer;

        //            var pcp = new SP_PROPCHANGE_PARAMS
        //                                           {
        //                                               ClassInstallHeader = new SP_CLASSINSTALL_HEADER
        //                                                                        {
        //                                                                            cbSize = Marshal.SizeOf(typeof(SP_CLASSINSTALL_HEADER)),
        //                                                                            InstallFunction = DIF.PropertyChange
        //                                                                        },
        //                                               StateChange = DICS_State,
        //                                               Scope = DICS_FLAG.Global,
        //                                               HwProfile = 0
        //                                           };

        //            //hDevInfo1 = SetupAPI.SetupDiGetClassDevs(Consts.GUID.DiskGUID, null, IntPtr.Zero,
        //            //                                         DIGCF.Present);

        //            hDevInfo1 = f_Handle;

        //            var hBuffer = default(GCHandle);
        //            DevInfoData.cbSize = Marshal.SizeOf(typeof(SP_DEVINFO_DATA));
        //            //var i = 0;
        //            const int Buffersize = 500;
        //            Buffer = new StringBuilder(Buffersize);
        //            try
        //            {
        //                hBuffer = GCHandle.Alloc(Buffer);
        //                try
        //                {
        //                    //while(SetupAPI.SetupDiEnumDeviceInfo(hDevInfo1, i, ref DevInfoData))
        //                    //{
        //                    var RequiredSize = 0;
        //                    var result = SetupAPI.SetupDiGetDeviceRegistryProperty(hDevInfo1,
        //                                                              ref DevInfoData,
        //                                                              SPDRP.CompatibleIDs, ref DataT, Buffer,
        //                                                              Buffer.Capacity, ref RequiredSize);
        //                    result.ToString();
        //                    if(Buffer.ToString() == @"USBSTOR\Disk")
        //                    {
        //                        if(!SetupAPI.SetupDiSetClassInstallParams(hDevInfo1, ref DevInfoData, ref pcp, Marshal.SizeOf(pcp)))
        //                            return Result;

        //                        if(!SetupAPI.SetupDiCallClassInstaller(DIF.PropertyChange, hDevInfo1, ref DevInfoData))
        //                            return Result;
        //                        Result = true;
        //                    }
        //                    //    i++;
        //                    //}
        //                }
        //                finally
        //                {
        //                    SetupAPI.SetupDiDestroyDeviceInfoList(hDevInfo1);
        //                }
        //            }
        //            finally
        //            {
        //                hBuffer.Free();
        //            }

        //            return Result;
        //            // ReSharper restore HeuristicUnreachableCode
        //#pragma warning restore 162
        //        }

        //
        // http://www.codeproject.com/KB/cs/HardwareHelper.aspx
        //
        //Name:     ChangeIt
        //Inputs:   pointer to hdev, SP_DEV_INFO, bool
        //Outputs:  bool
        //Errors:   This method may throw the following exceptions.
        //      Unable to change device state!
        //Remarks:  Attempts to enable or disable a device driver.  
        //      IMPORTANT NOTE!!!   This code currently does not check the reboot flag.
        //      =================   Some devices require you reboot the OS for the change
        //                  to take affect.  If this describes your device, you 
        //                  will need to look at the SDK call:
        //                  SetupDiGetDeviceInstallParams.  You can call it 
        //                  directly after ChangeIt to see whether or not you need 
        //                  to reboot the OS for you change to go into effect.
        public static bool ChangeIt(IntPtr hDevInfo, SP_DEVINFO_DATA devInfoData, bool Enable)
        {
            try
            {
                //Marshalling vars
                int lv_SizeOfPCP;
                IntPtr pPCP;
                int lv_SizeOfDevInfoData;
                IntPtr pDevInfoData;

                var pcp = new SP_PROPCHANGE_PARAMS();
                if(Enable)
                {
                    pcp.ClassInstallHeader.cbSize = Marshal.SizeOf(typeof(SP_CLASSINSTALL_HEADER));
                    pcp.ClassInstallHeader.InstallFunction = DIF.PropertyChange;
                    pcp.StateChange = DICS.Enable;
                    pcp.Scope = DICS_FLAG.Global;
                    pcp.HwProfile = 0;

                    //Marshal the params
                    lv_SizeOfPCP = Marshal.SizeOf(pcp);
                    pPCP = Marshal.AllocHGlobal(lv_SizeOfPCP);
                    Marshal.StructureToPtr(pcp, pPCP, true);
                    lv_SizeOfDevInfoData = Marshal.SizeOf(devInfoData);
                    pDevInfoData = Marshal.AllocHGlobal(lv_SizeOfDevInfoData);

                    if(SetupAPI.SetupDiSetClassInstallParams(hDevInfo, pDevInfoData, pPCP,
                                                             Marshal.SizeOf(typeof(SP_PROPCHANGE_PARAMS))))
                        SetupAPI.SetupDiCallClassInstaller(DIF.PropertyChange, hDevInfo, pDevInfoData);
                    pcp.ClassInstallHeader.cbSize = Marshal.SizeOf(typeof(SP_CLASSINSTALL_HEADER));
                    pcp.ClassInstallHeader.InstallFunction = DIF.PropertyChange;
                    pcp.StateChange = DICS.Enable;
                    pcp.Scope = DICS_FLAG.ConfigSpecific;
                    pcp.HwProfile = 0;
                }
                else
                {
                    pcp.ClassInstallHeader.cbSize = Marshal.SizeOf(typeof(SP_CLASSINSTALL_HEADER));
                    pcp.ClassInstallHeader.InstallFunction = DIF.PropertyChange;
                    pcp.StateChange = DICS.Disable;
                    pcp.Scope = DICS_FLAG.ConfigSpecific;
                    pcp.HwProfile = 0;
                }
                //Marshal the params
                lv_SizeOfPCP = Marshal.SizeOf(pcp);
                pPCP = Marshal.AllocHGlobal(lv_SizeOfPCP);
                Marshal.StructureToPtr(pcp, pPCP, true);
                lv_SizeOfDevInfoData = Marshal.SizeOf(devInfoData);
                pDevInfoData = Marshal.AllocHGlobal(lv_SizeOfDevInfoData);
                Marshal.StructureToPtr(devInfoData, pDevInfoData, true);

                var rslt1 = SetupAPI.SetupDiSetClassInstallParams(hDevInfo, pDevInfoData, pPCP,
                    Marshal.SizeOf(typeof(SP_PROPCHANGE_PARAMS)));
                var rstl2 = SetupAPI.SetupDiCallClassInstaller(DIF.PropertyChange, hDevInfo, pDevInfoData);
                if((!rslt1) || (!rstl2))
                    throw new Exception("Unable to change device state!");
                return true;
            }
            catch(Exception)
            {
                return false;
            }
        }

        /* ------------------------------------------------------------------------------------------ */

        public bool Equals(Device other) { return other != null && other.f_Handle == f_Handle; }

        public override string ToString() { return string.Format("{0}({1}):{2:x}", FrendlyName, EnumeratorName, f_Handle.ToInt32()); }
        public override int GetHashCode() { return f_Handle.GetHashCode(); }

        public override bool Equals(object obj) { return obj is Device && Equals(obj as Device); }

        /* ------------------------------------------------------------------------------------------ */

        public static bool operator !=(Device a, Device b) { return !(a == b); }
        public static bool operator ==(Device a, Device b)
        {
            return ReferenceEquals(a, null) && ReferenceEquals(b, null)
                || !ReferenceEquals(a, null) && a.Equals(b);
        }

        /* ------------------------------------------------------------------------------------------ */
    }
}